package org.chronologger.poppaea.client;

import java.util.Date;
import java.util.HashMap;
import java.util.List;

import org.chronologger.poppaea.client.domain.ChronologyData;
import org.chronologger.poppaea.client.domain.Location;
import org.chronologger.poppaea.client.domain.Scene;
import org.chronologger.poppaea.client.overlap.OverlapResolver;
import org.chronologger.poppaea.client.utils.Point;
import org.chronologger.poppaea.client.widgets.EventAwareAbsolutePanel;
import org.chronologger.poppaea.client.widgets.EventHandler;
import org.chronologger.poppaea.client.widgets.SceneWidget;
import org.chronologger.poppaea.client.widgets.LocationBarWidget;
import org.chronologger.poppaea.client.widgets.SegmentedButton;
import org.chronologger.poppaea.client.widgets.TimelineWidget;

import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.ScrollEvent;
import com.google.gwt.event.dom.client.ScrollHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ScrollPanel;

import static org.chronologger.poppaea.client.widgets.LocationWidget.LOCATION_WIDGET_HEIGHT;
import static org.chronologger.poppaea.client.widgets.SceneWidget.MOVEMENT_STEP;

public class Chronology extends Composite {

	private static final int MINIMUM_RESOLUTION = 25;
	public static final int LOCATION_WIDGET_GAP = 4;
	public static final int LOCATION_PANEL_WIDTH = 100;
	public static final int TIMELINE_PANEL_HEIGHT = 30;

	/**
	 * data
	 */

	private final ChronologyData data;
	private final HashMap<Location, OverlapResolver> overlapResolvers = new HashMap<Location, OverlapResolver>();
	private PositionResolver positionResolver;

	/**
	 * UI
	 */

	private final AbsolutePanel mainPanel = new AbsolutePanel();
	private final EventAwareAbsolutePanel scenesPanel = new EventAwareAbsolutePanel();
	private final ScrollPanel scenesScrollPanel = new ScrollPanel(
			getScenesPanel());

	private final TimelineWidget timeline = new TimelineWidget(this);
	private final LocationBarWidget locationBar;

	private final SegmentedButton zoomButton = new SegmentedButton();
	private final SegmentedButton closeButton = new SegmentedButton();
	private final SegmentedButton addLocationButton = new SegmentedButton();

	public Chronology(ChronologyData data) {
		this.data = data;
		locationBar = new LocationBarWidget(data);
		positionResolver = new LinearPositionResolver(data);

		data.startTime = new Date(0);
		data.endTime = new Date(1000000000L);

		initWidget(mainPanel);
		mainPanel.setSize("100%", "100%");

		Label topLeft = new Label("");

		// initialize sizes
		locationBar.setWidth(LOCATION_PANEL_WIDTH + "px");
		topLeft.setSize(LOCATION_PANEL_WIDTH + "px", TIMELINE_PANEL_HEIGHT
				+ "px");

		// initialize styles
		getScenesPanel().setStyleName("scenes-panel");
		topLeft.setStyleName("opaque-background");

		// add widgets to GUI
		mainPanel.add(timeline, LOCATION_PANEL_WIDTH, 0);
		mainPanel.add(scenesScrollPanel, LOCATION_PANEL_WIDTH,
				TIMELINE_PANEL_HEIGHT);
		mainPanel.add(locationBar, 0, TIMELINE_PANEL_HEIGHT);
		mainPanel.add(topLeft, 0, 0);
		mainPanel.add(zoomButton, 4, 4);

		closeButton.addSegment("close");
		mainPanel.add(closeButton, 50, 4);

		addLocationButton.addSegment(LocationBarWidget.lang.addLocation())
				.addClickHandler(new ClickHandler() {

					public void onClick(ClickEvent event) {
						addLocation("");
					}
				});

		addLocationButton.setSegmentSize(0, Chronology.LOCATION_PANEL_WIDTH - 2
				- Chronology.LOCATION_WIDGET_GAP * 2, 20);

		mainPanel.add(addLocationButton, LOCATION_WIDGET_GAP - 2,
				TIMELINE_PANEL_HEIGHT);

		// create DoubleClick handler on scenesPanel for creating new scenes
		final EventHandler scenesPanelDoubleClick = new EventHandler() {

			public void fire(Event event) {
				final int x = event.getClientX()
						- getScenesPanel().getAbsoluteLeft();
				final int y = event.getClientY()
						- getScenesPanel().getAbsoluteTop();

				addScene(x, y);
			}
		};
		scenesPanel.addEventHandler(Event.ONDBLCLICK, scenesPanelDoubleClick);

		final ScrollHandler scenesScrollHandler = new ScrollHandler() {
			public void onScroll(ScrollEvent event) {
				onScrollPositionChanged();
			}
		};
		scenesScrollPanel.addScrollHandler(scenesScrollHandler);

		// add ClickHandlers to zoom buttons
		final ClickHandler zoomOutHandler = new ClickHandler() {
			public void onClick(ClickEvent event) {
				setPreferredResolution(positionResolver
						.getPreferredResolution() / 2);
			}
		};

		zoomButton.addSegment("-").addClickHandler(zoomOutHandler);
		zoomButton.getSegment(0).setWidth("10px");

		final ClickHandler zoomInHandler = new ClickHandler() {
			public void onClick(ClickEvent event) {
				setPreferredResolution(positionResolver
						.getPreferredResolution() * 2);
			}
		};
		zoomButton.addSegment("+").addClickHandler(zoomInHandler);
		zoomButton.getSegment(1).setWidth("10px");

		// create widgets
		for (Location loc : data.locations) {
			// create overlap resolver
			OverlapResolver newOverlapResolver = new OverlapResolver();
			newOverlapResolver.setHeight(LOCATION_WIDGET_HEIGHT);
			overlapResolvers.put(loc, newOverlapResolver);

			for (Scene scene : loc) {
				scene.setLocation(loc);
				createWidgetForScene(scene);
			}
		}

		setPreferredResolution(100);

		DeferredCommand.addCommand(new Command() {

			public void execute() {
				locationBar.createWidgetsForData();
				getScenesPanel().setHeight(
						locationBar.getOffsetHeight() + 4 + "px");

				mainPanel.setWidgetPosition(addLocationButton,
						LOCATION_WIDGET_GAP - 2, locationBar.getAbsoluteTop()
								+ locationBar.getOffsetHeight()
								+ LOCATION_WIDGET_GAP);
			}
		});
	}

	/**
	 * Adds a new scene to the chronology and updates the user interface
	 * (creates new SceneWidget).
	 * 
	 * @param x
	 *            x position, relative to the scenes panel
	 * @param y
	 *            y position, relative to the scenes panel
	 */
	public void addScene(final int x, final int y) {
		// get corresponding location
		Location location = getLocations().get(
				y / (LOCATION_WIDGET_HEIGHT + LOCATION_WIDGET_GAP));

		// calculate positions on timeline
		final int startPos = (x + MOVEMENT_STEP / 2) / MOVEMENT_STEP
				* MOVEMENT_STEP;
		final int endPos = (x + MOVEMENT_STEP / 2) / MOVEMENT_STEP
				* MOVEMENT_STEP + 100;

		// create new scene
		Scene newScene = location.addScene();
		newScene.setLocation(location);
		newScene.setStartTime(positionResolver.getTime(startPos));
		newScene.setEndTime(positionResolver.getTime(endPos));
		newScene.updateOnServer();

		createWidgetForScene(newScene);
	}

	private void createWidgetForScene(Scene scene) {
		// create SceneWidget and add it to the user interface
		SceneWidget sceneWidget = new SceneWidget(scene, this);
		updateSceneWidgetPosition(sceneWidget);
	}

	/**
	 * Takes a point in client coordinates (Position on the screen relative to
	 * top left of window) and returns the corresponding point in the scenes
	 * panel's coordinate system.
	 * 
	 * @param p
	 *            point in client coordinates
	 * @return
	 */
	public Point clientToScenesPanelPosition(Point p) {
		Point result = new Point();
		result.x = p.x - scenesScrollPanel.getAbsoluteLeft()
				+ scenesScrollPanel.getHorizontalScrollPosition();
		result.y = p.y - scenesScrollPanel.getAbsoluteTop()
				+ scenesScrollPanel.getScrollPosition();
		return result;
	}

	/**
	 * Takes a point in client coordinates and returns the corresponding point
	 * in scenes panel client coordinates
	 * 
	 * @param p
	 * @return
	 */
	public Point clientToScenesPanelClient(Point p) {
		Point result = new Point();
		result.x = p.x - scenesScrollPanel.getAbsoluteLeft();
		result.y = p.y - scenesScrollPanel.getAbsoluteTop();
		return result;
	}

	/**
	 * Call this when resizing the timeline
	 * 
	 * @param width
	 * @param height
	 */
	public void resize(int width, int height) {
		scenesScrollPanel.setSize(width - LOCATION_PANEL_WIDTH + "px", height
				- TIMELINE_PANEL_HEIGHT + "px");
	}

	/**
	 * Sets the timeline's preferred resolution and updates the position of all
	 * scene widgets.
	 * 
	 * @param pixelsPerDay
	 */
	public void setPreferredResolution(int pixelsPerDay) {
		final int halfWidth = scenesScrollPanel.getOffsetWidth() / 2;
		final Long time = positionResolver.getTime(scenesScrollPanel
				.getHorizontalScrollPosition()
				+ halfWidth);

		if (pixelsPerDay < MINIMUM_RESOLUTION)
			pixelsPerDay = MINIMUM_RESOLUTION;
		positionResolver.setPreferredResolution(pixelsPerDay);
		updateTimeline();
		updateScenePositions();

		final int position = positionResolver.getPosition(time);
		scenesScrollPanel.setHorizontalScrollPosition(position - halfWidth);
	}

	/**
	 * Sets the timeline's start time and updates the position of all events.
	 * 
	 * @param start
	 */
	public void setStarttime(Date start) {
		data.startTime = start;
		updateTimeline();
		updateScenePositions();
	}

	/**
	 * Sets the timeline's end time and updates the position of all events.
	 * 
	 * @param end
	 */
	public void setEndtime(Date end) {
		data.endTime = end;
		updateTimeline();
		updateScenePositions();
	}

	public Date getStarttime() {
		return data.startTime;
	}

	public Date getEndtime() {
		return data.endTime;
	}

	/**
	 * Updates the timeline (not the events panel)
	 */
	protected void updateTimeline() {
		timeline.update();
		getScenesPanel().setWidth(timeline.getOffsetWidth() + "px");
	}

	/**
	 * Updates the position of a single EventWidget
	 * 
	 * @param sceneWidget
	 */
	public void updateSceneWidgetPosition(SceneWidget sceneWidget) {
		// if it's not already a child of the eventsPanel, make it one
		if (sceneWidget.getParent() != getScenesPanel())
			getScenesPanel().add(sceneWidget);

		// get start and end position in pixels
		Scene scene = sceneWidget.getScene();
		int startPos = positionResolver.getPosition(scene.getStartTime());
		int endPos = positionResolver.getPosition(scene.getEndTime());

		OverlapResolver resolver = getOverlapResolver(scene.getLocation());

		// what location does this eventWidget belong to?
		int locationIndex = getLocations().indexOf(scene.getLocation());
		getScenesPanel().setWidgetPosition(
				sceneWidget,
				startPos,
				locationIndex * (LOCATION_WIDGET_HEIGHT + LOCATION_WIDGET_GAP)
						+ resolver.getTop(scene));

		sceneWidget.setPixelSize(endPos - startPos, resolver.getHeight(scene));
	}

	/**
	 * Updates the position of all EventWidgets
	 */
	public void updateScenePositions() {
		for (Location loc : data.locations) {
			for (Scene scene : loc) {
				updateSceneWidgetPosition(scene.getWidget());
			}
		}
	}

	public void addLocation(String name) {
		Location newLocation = locationBar.addLocation(name);
		OverlapResolver newOverlapResolver = new OverlapResolver();
		newOverlapResolver.setHeight(LOCATION_WIDGET_HEIGHT);
		overlapResolvers.put(newLocation, newOverlapResolver);
		getScenesPanel().setHeight(locationBar.getOffsetHeight() + 4 + "px");

		mainPanel.setWidgetPosition(addLocationButton, LOCATION_WIDGET_GAP - 2,
				locationBar.getAbsoluteTop() + locationBar.getOffsetHeight()
						+ LOCATION_WIDGET_GAP);
	}

	public void setPositionResolver(PositionResolver positionResolver) {
		this.positionResolver = positionResolver;
	}

	public PositionResolver getPositionResolver() {
		return positionResolver;
	}

	public List<Location> getLocations() {
		return data.locations;
	}

	/**
	 * updates the positions of the location bar and the timeline after the
	 * EventPanel was scrolled.
	 */
	private void onScrollPositionChanged() {
		final int scrollPosY = scenesScrollPanel.getScrollPosition();
		mainPanel.setWidgetPosition(locationBar, 0, TIMELINE_PANEL_HEIGHT
				- scrollPosY);

		final int scrollPosX = scenesScrollPanel.getHorizontalScrollPosition();
		mainPanel.setWidgetPosition(timeline,
				LOCATION_PANEL_WIDTH - scrollPosX, 0);

		mainPanel.setWidgetPosition(addLocationButton, LOCATION_WIDGET_GAP - 2,
				locationBar.getAbsoluteTop() + locationBar.getOffsetHeight()
						+ LOCATION_WIDGET_GAP);
	}

	public EventAwareAbsolutePanel getScenesPanel() {
		return scenesPanel;
	}

	public ScrollPanel getScenesScrollPanel() {
		return scenesScrollPanel;
	}

	public OverlapResolver getOverlapResolver(Location location) {
		return overlapResolvers.get(location);
	}

	public void addCloseHandler(ClickHandler handler) {
		closeButton.getSegment(0).addClickHandler(handler);
	}

}